﻿using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Diagnostics;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System;

namespace TapMe.Model
{
    /// <summary>
    /// Model class for the game. Emits an array of randomly generated 3-tuples (a, b, isFake), where a, b are integers, 
    /// and isFake is a boolean (true if this point is a fake point, false otherwise). Uses Singleton design pattern. 
    /// </summary>
    /// <remarks>
    ///     To reset the mapping between level number and the number of points per task in that level, change the private
    ///     function mapLevelNumToNumPtsPerTask
    /// </remarks>
    class Model : INotifyPropertyChanged
    {

        #region private members

        // instance of this class
        private static Model instance = null;
        // the latest emitted pattern
        private IList<PatternPt> currentPattern;
        // the number of values the first coordinate of a Tuple can take
        // (if the first coordinate is a boolean, this number should always be 2)
        private int firstCoordCardinality;
        // the number of values the second coordinate of a Tuple can take
        private int secondCoordCardinality;
        // the probability that the third component of a randomly generated Tuple will be FALSE (fake),
        // given that the previous Tuple in the pattern is not fake
        private double probNotFakeToFake;
        // the random number generator
        private Random randomNumberGtor;
                
        // an index into the container currentPattern. Used for keeping track of the points
        // that has to be verified
        private int currentPointIndex;
        // the current task index
        private int currentTaskIndex;
        // the stopwatch used to raise verification timout events
        private ExpirableStopwatch stopwatch;

        #endregion

        #region event members
        // functions hooked up to this event will be called 
        // when pattern verification timeout occurs
        public event EventHandler verificationTimeoutEventHandler;
        #endregion

        #region Implementation of INotifyPropertyChanged
        // reference http://msdn.microsoft.com/en-us/library/System.ComponentModel.INotifyPropertyChanged.aspx
        // Implements INotifyPropertyChanged
        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        #endregion

        #region Properties
        // NOTE: in implementing this class, the private fields should only be set via public properties
        // so that appropriate events can be raised (the only exception is in the constructor(s))

        private bool verificationExpiredValue;

        // this value is updated whenever level elapsed time and number of successfully completed tasks changes
        private int levelScoreValue;

        // the total number of time (in ms) spent on tapping out patterns in a single level
        private long levelElapsedTimeValue;

        // the total number of successfully completed tasks within one level
        private int levelNumSuccessTasksValue;

        private bool inTaskValue;

        // the current level number
        private int currentLevelNumberValue;

        public bool VerificationExpired
        {
            get
            {
                return this.verificationExpiredValue;
            }
            protected set
            {
                if (value != this.verificationExpiredValue)
                {
                    this.verificationExpiredValue = value;
                    NotifyPropertyChanged();
                }
            }
        }

        public int LevelScore
        {
            get
            {
                return this.levelScoreValue;
            }
            protected set
            {
                if (value != this.levelScoreValue)
                {
                    this.levelScoreValue = value;
                    NotifyPropertyChanged();
                }
            }
        }

        public long LevelElapsedTime
        {
            get
            {
                return this.levelElapsedTimeValue;
            }
            protected set
            {
                if (value != this.levelElapsedTimeValue)
                {
                    this.levelElapsedTimeValue = value;
                    calculateCurrentLevelScore();
                    NotifyPropertyChanged();
                }
            }
        }

        public int LevelNumSuccessTasks
        {
            get
            {
                return this.levelNumSuccessTasksValue;
            }
            protected set
            {
                if (value != this.levelNumSuccessTasksValue)
                {
                    this.levelNumSuccessTasksValue = value;
                    calculateCurrentLevelScore();
                    NotifyPropertyChanged();
                }
            }
        }

        public bool InTask
        {
            get
            {
                return this.inTaskValue;
            }
            protected set
            {
                if (value != this.inTaskValue)
                {
                    this.inTaskValue = value;
                    NotifyPropertyChanged();
                }
            }
        }

        public int CurrentLevelNumber
        {
            get
            {
                return this.currentLevelNumberValue;
            }
            protected set
            {
                if (value != this.currentLevelNumberValue)
                {
                    this.currentLevelNumberValue = value;
                    NotifyPropertyChanged();
                }
            }
        }

        #endregion

        #region Public methods
        /// <summary>
        /// Call this method to get the model
        /// </summary>
        /// <param name="firstCoordCardinality">
        ///     The number of values the first coordinate of a point can take. 
        ///      If the first coordinate is a boolean, this number should always be 2
        /// </param>
        /// <param name="secondCoordCardinality"></param>
        /// <param name="probabilityFalse"></param>
        /// <returns></returns> 
        public static Model getModelInstance(int firstCoordCardinality, int secondCoordCardinality, double probabilityFalse)
        {
            if (instance == null)
            {
                instance = new Model(firstCoordCardinality, secondCoordCardinality, probabilityFalse);
            }
            return instance;
        }

        // private constructor        
        private Model(int firstCoordCardinality, int secondCoordCardinality, double probabilityFalse)
        {
            this.currentPattern = new List<PatternPt>();
            this.firstCoordCardinality = firstCoordCardinality;
            this.secondCoordCardinality = secondCoordCardinality;
            this.probNotFakeToFake = probabilityFalse;
            this.randomNumberGtor = new Random();
            this.currentPointIndex = 0;
            this.currentTaskIndex = 0;
            this.stopwatch = null;
            this.currentLevelNumberValue = 0;
            this.verificationExpiredValue = false;
            this.levelScoreValue = 0;
            this.levelElapsedTimeValue = 0;
            this.levelNumSuccessTasksValue = 0;
            this.inTaskValue = false;
            this.currentLevelNumberValue = 0;            
        }

        /// <summary>
        ///     An internal stopwatch object will be instantiated and started
        ///     right before this method returns. The event "verificationTimeout"
        ///     will be raised whenever the stopwatch indicates that the method
        ///     verifyPattern has not been called for too long.
        ///     The size of the returned list will depend on the current level number
        /// </summary>
        public IList<PatternPt> generatePattern()
        {
            int numPoints = mapLevelNumToNumPtsPerTask( this.currentLevelNumberValue );
            return generatePatternHelper(numPoints);
        }
       
        /// <param name="numPoints">A positive integer</param>        
        private IList<PatternPt> generatePatternHelper(int numPoints)
        {
            this.resetTaskStates();

            // the phantom point is not a part of the pattern; it is created merely so
            // that we have something to pass into the generatePoint method            
            // Here, we assume that the first point is preceded by a non-fake "phantom" point
            PatternPt phantom = new PatternPt(true, 0, false);
                 
            // get the first point in the pattern
            PatternPt prev = generatePoint(phantom);
                        
            this.currentPattern.Add(prev);

            PatternPt cur = null;

            for (int i = 1; i < numPoints; i++)
            {
                // repeatedly get a new point until at least one of its first two attributes
                // is different from that of the previous point
                // note that this piece of code can be implemented in the generatePoint method too
                do
                {
                    cur = generatePoint(prev);
                } while (cur.isShort == prev.isShort && cur.numFingers == prev.numFingers);
                this.currentPattern.Add(cur);
                prev = cur;
            }

            // advances currentPointIndex to a non-bomb point (if there are no more non-bomb points beyond
            // advance currentPointIndex so that hasNextPatternPt() will return false )
            while (currentPointIndex < currentPattern.Count && currentPattern[currentPointIndex].isBomb)
            {
                currentPointIndex++;
            }

            // prepare the ExpirableStopwatch object to enable verification timeout
            this.stopwatch = new ExpirableStopwatch();

            // hook up the verificationStopWatchExpired function to the Expired event on the stopwatch
            stopwatch.Expired += verificationStopWatchExpired;

            return new List<PatternPt>(this.currentPattern);
        }

        /// <summary>
        ///   Verifies the input against the currentPattern.
        ///   When this method is called, the timer controlling 
        ///   the verificationTimout event will be paused, and 
        ///   will be restarted only if the input is correct, and
        ///   there are still non-Fake points left in currentPattern
        /// </summary>
        /// <requires>
        ///     the model is currently in a task (as indicated by InTask)
        /// </requires>
        /// <changes>
        ///     this.LevelElapsedTime
        ///     this.LevelNumSuccessTasks
        /// </changes>
        /// <throws>
        ///     InvalidOperationException
        /// </throws>
        /// <param name="other"></param>
        /// <returns></returns>
        public bool verifyPattern(PatternPt other)
        {
            if (!InTask)
            {
                resetTaskStates();
                throw new InvalidOperationException("Called verifyPattern() without entering a task first "
                    + "(To start a new task, call startTask() )");                
            }

            if (stopwatch == null)
            {
                return false;
            }

            stopwatch.ExpirableStop();
            
            // updates elapsed time
            this.LevelElapsedTime = stopwatch.ElapsedMilliseconds;

            if (this.hasNextNonFakePatternPt())
            {
                PatternPt nextPt = this.getNextNonFakePatternPt();
                if (nextPt.isShort == other.isShort && nextPt.numFingers == other.numFingers)
                {
                    // input point is correct
                                        
                    if (!this.hasNextNonFakePatternPt())
                    {
                        // reached the last nonbomb point and
                        // successfully completed the task
                        this.InTask = false;
                        this.LevelNumSuccessTasks++;
                    }
                    else
                    {
                        stopwatch.ExpirableStart();                        
                    }
                    return true;
                }
                else
                {
                    // input point is incorrect
                    this.InTask = false;
                    return false;
                }
            }
            else
            {
                // already read past the last nonbomb point
                // but verifyPattern is still called
                return false;
            }
        }

        /// <summary>
        /// Resets all the internal states of the Model object
        /// </summary>
        public void resetAllStates()
        {
            resetTaskStates();
            resetLevelStates();
        }
                
        /// <summary>
        /// Starts a task and an internal timer. After this method is called, 
        /// the owner of the Model needs toINotifyPropertyChanged call verifyPattern within every 2 seconds
        /// (if longer than 2 seconds elapsed before verifyPattern is called, 
        /// the property VerificationExpired will be changed, an event will be raised,
        /// and the InTask will become false
        /// </summary>
        public void startTask()
        {            
            this.InTask = true;
            stopwatch.Start();
        }

        // resets the model and start a level
        public void startLevel(int levelNumber)
        {
            resetAllStates();
            this.CurrentLevelNumber = levelNumber;            
        }

        #endregion

        #region Methods releated to events

        protected virtual void onVerificationTimeout(EventArgs e)
        {
            if (verificationTimeoutEventHandler != null)
            {
                verificationTimeoutEventHandler(this, e);
            }
        }

        // this function is hooked up to the Expire event on the stopwatch object
        private void verificationStopWatchExpired(object sender, EventArgs e)
        {
            //Console.WriteLine("TIMEOUT!!!");

            // for the good ol' event types
            onVerificationTimeout(EventArgs.Empty);

            // for INotifyPropertyChanged
            this.VerificationExpired = true;

            // stop current task
            this.InTask = false;
        }

        #endregion

        #region helper functions

        /// <summary>
        /// Generate a point for a pattern. The new point wiil depend on state on the previous point
        /// </summary>
        /// <param name="prevPt">The point in currentPattern immediately preceding the point to be generated</param>
        /// <returns>The new PatternPt</returns>
        private PatternPt generatePoint(PatternPt prevPt)
        {
            int firstCoord = randomNumberGtor.Next(firstCoordCardinality);
            int secondCoord = randomNumberGtor.Next(secondCoordCardinality);

            // the transition probability to a fake point, given the state of the previous point
            // if the previous point is already fake, this point will NOT be fake
            // if the previous point is NOT fake, this point will be fake with probability probNotFakeToFake
            double transitionToFake = prevPt.isBomb ? 0 : this.probNotFakeToFake;

            // gets a number uniformly between zero and one
            double sample = randomNumberGtor.NextDouble();

            bool isFake = (sample < transitionToFake);

            return new PatternPt( (firstCoord == 1), secondCoord, isFake );
        }

        /// <summary>
        /// Check whether there are any non-bomb points left in currentPattern
        /// </summary>
        /// <returns></returns>
        private bool hasNextNonFakePatternPt()
        {
            return currentPointIndex < currentPattern.Count;
        }

        /// <summary>
        /// Return a non-bomb PatternPoint in currentPattern
        /// and advance currentPointIndex to the next non-bomb PatternPoint
        /// (or, in case that there are no more non-bomb points left,
        /// advance currentPointIndex so that hasNextPatternPt() will return false)
        /// </summary>
        /// <returns></returns>
        private PatternPt getNextNonFakePatternPt()
        {
            PatternPt result; 
            if (currentPointIndex >= currentPattern.Count)
            {
                result = null;
            }
            else
            {
                result = currentPattern[currentPointIndex];
            }

            // advances currentPointIndex to a non-bomb point (if there are no more non-bomb points beyond
            // advances currentPointIndex so that hasNextPatternPt() will return false )
            currentPointIndex++;

            while (currentPointIndex < currentPattern.Count && currentPattern[currentPointIndex].isBomb)
            {
                currentPointIndex++;
            }

            return result;
        }

        // reset the members related to the current task
        private void resetTaskStates()
        {
            this.stopwatch.Stop();
            this.stopwatch = null;

            this.currentPattern.Clear();
            this.VerificationExpired = false;
            this.currentPointIndex = 0;
        }
        
        // reset the members related to the current level
        // the level number is reset to one
        private void resetLevelStates()
        {
            this.currentTaskIndex = 0;
            this.LevelScore = 0;
            this.LevelElapsedTime = 0;
            this.LevelNumSuccessTasks = 0;
            this.CurrentLevelNumber = 1;
        }

        // convert the level number to the number of points per task in that level
        private int mapLevelNumToNumPtsPerTask(int levelNumber)
        {
            return levelNumber + 4;
        }

        // sets the current score based on the current elapsed time and the number of successfully 
        // completed tasks. This method should be called whenever the Set accessor of elapsed time
        // and/or the Set accessor of succesfully completed tasks are called
        // TODO: update the formula to reflect the number of points per level etc.
        private void calculateCurrentLevelScore()
        {   
            this.LevelScore = -(int) this.LevelElapsedTime + this.LevelNumSuccessTasks * 1000;
        }
        #endregion
    }


    /// <summary>
    /// A stopwatch object that raises events whenever it has been in
    /// Start state for more than a certain period of time
    /// 
    /// References: http://msdn.microsoft.com/en-us/library/aa645739(v=vs.71).aspx
    /// </summary>
    class ExpirableStopwatch : Stopwatch
    {
        // functions hooked up to this event are called whenever this
        // object has been in start state for too long
        public event EventHandler Expired;

        // used to record the ElapsedMilliseconds property when the 
        // stopwatch just entered the start state
        private long curElapsed;

        // executes callBack periodically to check if been in start
        // state for too long
        private Timer waiter = null;

        protected virtual void OnExpired(EventArgs e)
        {
            if (Expired != null)
            {
                Expired(this, e);
            }
        }

        // called periodically to check if been in start state for too long
        private void callBack(object state)
        {
            if (this.ElapsedMilliseconds - curElapsed > 2000)
            {
                //Console.WriteLine("Wait time exceeded 2000 milliseconds. Firing event...");
                OnExpired(EventArgs.Empty);
            }
        }


        /// <summary>
        ///   Call this method instead of Start() so that the event "Expired" will be raised
        ///   if been in start state for too long
        /// </summary>
        /// <requires>
        ///   If this method is called, ExpirableStop must be eventually called on this object
        /// </requires>
        public void ExpirableStart()
        {
            base.Start();
            curElapsed = this.ElapsedMilliseconds;
            waiter = new Timer(this.callBack, null, 1000, 1000);
        }

        /// <summary>
        ///   Stop the stopwatch and disposes of the Threading.Timer object
        ///   Calling ExpirableStop on a stop watch that is not running will
        ///   not change anything
        /// </summary>
        public void ExpirableStop()
        {
            base.Stop();
            if (waiter != null)
            {
                waiter.Dispose();
                waiter = null;
            }
        }
    }
}
